#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@Project ：prod_ctrl_service 
@File    ：input_wip_instation.py
@IDE     ：PyCharm 
@Author  ：孔令伟
@Date    ：8/23/24 11:11 AM 
"""
import json
from typing import Dict, List, Tuple, Union

from mg_app_framework import MesCode, get_logger, get_organization

from common.basic_mongo import get_mongo_collection, get_retrospect_collection
from common.basic_redis import get_redis_conn
from common.constant import RetrospectType
from common.retrospect_operate.handle import publish_retrospect_nodes
from prod_ctrl_service.tool.tool import (check_basic_param, check_repeat_in_or_out, get_feeding_material_code_num_map,
                                         get_time, device_workorder, batch_make_retros_in,
                                         aggregate_unqualified_records, update_unqualified_records, check_wip_no_exists)

"""
######## IF022
生产过程-在制品进站-电芯信息校验
设备生产启动前，MES系统验证在制品通过后（品质校验、Marking校验、是否符合工艺流程、BOM所需等）、登记当前投料蓝胶码/电芯码。
上位机获取在制品信息及校验结果，异常需要排出或报警
"""


async def input_wip_instation_check(
        params: Dict[str, Union[str, List[Dict[str, str]]]]
) -> Dict[str, Union[str, Dict]]:
    """
    设备生产启动前，MES系统验证在制品通过后（品质校验、Marking校验、是否符合工艺流程、BOM所需等）、登记当前投料蓝胶码/电芯码。
    上位机获取在制品信息及校验结果，异常需要排出或报警
    :param params:{
                "call_id":"JQ00386",  #接口调用方认证ID, string
                "trace_id":"8941d16b-0581-4e68-8940-a651afff4282",  #接口调用ID, string
                "upperpc_code": "UP011",    #上位机编码, string
                "user_code":"Y001",  #员工编码, string
                "equip_code":"ZP001",   #设备编码, string
                "process_code":"2050",   # 工序编码, string, 必输
                "wip_list":[    # 在制品列表, 必输
                              {"wip_no":"WW202408140001"},   #在制品单件码
                              {"wip_no":"WW202408140002"},
                              {"wip_no":"WW202408140003"},
                              {"wip_no":"WW202408140004"}
                           ]
             }
    :return:{
                "code": "success",   #接口应答状态代码, success：成功; fail：失败, string
                "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
                "info": "OK"   #接口返回信息描述, 如果为fail, 则为fail的异常描述信息, string
             }
    """
    error_msg = ""
    # '接口调用方认证ID/接口调用ID/上位机编码/员工编码/设备编码'是每一个接口都有且必填的入参，检查这些参数是否填写
    basic_param_check_result, basic_param_check_message = check_basic_param(params=params)
    if not basic_param_check_result:
        error_msg += basic_param_check_message
    wip_list = params.get("wip_list", {})
    if not wip_list:
        error_msg += "传入参数'在制品列表(wip_list)'未填写;"
    if error_msg:
        result = {
            "code": MesCode.fail,  # 接口应答状态代码, success：成功; fail：失败, string
            "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
            "info": error_msg,  # 接口返回信息描述, 如果为fail, 则为fail的异常描述信息, string
        }
        return result
    process_code = params.get("process_code")
    wip_no_list = [rec["wip_no"] for rec in wip_list]
    # 判断工序process_code是不是C100，如果是C100，需要修改成以下逻辑，对每一个进站码wip_no做一个转换
    if process_code == "C100":
        check_rule_result, origin_code_new_code_map, check_rule_msg = await check_wip_no_exists(wip_no_list)
        if not check_rule_result:
            error_msg += check_rule_msg
        else:
            wip_no_list = list(origin_code_new_code_map.values())
    # 对传入的码值进行不合格品记录校验; re_input_wip : 复投的码值
    unqualified_check_msg, re_input_wip = await aggregate_unqualified_records(wip_no_list, process_code)
    if unqualified_check_msg:
        error_msg += unqualified_check_msg
        result = {
            "code": MesCode.fail,  # 接口应答状态代码, success：成功; fail：失败, string
            "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
            "info": error_msg,  # 接口返回信息描述, 如果为fail, 则为fail的异常描述信息, string
        }
        return result
    # bom信息
    equip_code = params.get("equip_code")
    rd_conn = get_redis_conn()
    ts_key = f"workorder_mgm~{equip_code}"
    bom_info = await rd_conn.get(ts_key)
    bom_info = json.loads(bom_info)
    workorder_status = bom_info.get("workorder_status")
    # 通过bom信息校验当前工序工单状态
    if workorder_status != "on_work":
        error_msg += f"当前工序无生产中工单;"
    pre_procedure_code: list = bom_info.get("pre_procedure_code", [])

    # 当前工序重码校验(一些码值本次进站操作属于复投，不需要校验是否重复进站)
    qr_code_for_check_repeat_in: list = list(set(wip_no_list) - set(re_input_wip))
    qr_code_has_used = await check_repeat_in_or_out(
        process_code, qr_code_for_check_repeat_in, RetrospectType.PRODUCT_IN
    )
    if qr_code_has_used:
        error_msg += f"下列在制品单件码重复进站:{qr_code_has_used};"

    # no_last_retros_code : 在前工序上没有追溯数据的单件码列表
    # retrospect_records : 每一个在制品单间码与它在前工序上的最新一条追溯数据的映射关系
    retrospect_records, no_last_retros_code = await get_last_retrospect(
        last_procedure_code_list=pre_procedure_code, qr_code_list=wip_no_list
    )
    # 前工序没有产出追溯信息校验
    if no_last_retros_code:
        error_msg += f"下列在制品单间码前工序没有产出追溯信息:{no_last_retros_code};"
    # 校验在制品单件码在前道工序生产的物料信息是否与当前工序bom信息中投入的在制品信息是否一致；
    # 查询前一道工序追溯数据的check_result是否合格
    # BOM齐套性校验
    # 原材料批次码的投料数量是否满足bom投入物料中各批次码的扣料数量
    check_result, bom_check_result_msg = await check_material(
        retrospect_records=retrospect_records,
        bom_info=bom_info,
        process_code=process_code,
    )
    if not check_result:
        error_msg += bom_check_result_msg

    # todo Marking校验, marking相关功能未开发，等待开发后补充marking校验功能

    if error_msg:
        result = {
            "code": MesCode.fail,  # 接口应答状态代码, success：成功; fail：失败, string
            "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
            "info": error_msg,  # 接口返回信息描述, 如果为fail, 则为fail的异常描述信息, string
        }
        return result

    # 进站时创建进站的追溯数据，这部分功能后续修改为调用提供的进站数据追溯数据创建接口
    device_code = params.get("equip_code")
    work_order = await device_workorder(device_code)
    insert_data_list = batch_make_retros_in(wip_list=wip_no_list, work_order=work_order)
    try:
        for rec_item in insert_data_list:
            await publish_retrospect_nodes(device_code=device_code, retrospect_items=[rec_item])
        get_logger().info(f"码值{wip_list}进站追溯数据已建立")
    except ValueError as e:
        error_msg += str(e)

    if error_msg:
        result = {
            "code": MesCode.fail,  # 接口应答状态代码, success：成功; fail：失败, string
            "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
            "info": error_msg,  # 接口返回信息描述, 如果为fail, 则为fail的异常描述信息, string
        }
        return result
    # 在进站后更新不合格品记录信息
    await update_unqualified_records(wip_no_list, process_code)

    # 在制品进站的入参每一次调用成功后存储
    input_data = {
        "call_id": params.get("call_id"),  # 接口调用方认证ID, string
        "trace_id": params.get("trace_id"),  # 接口调用ID, string
        "upperpc_code": params.get("upperpc_code"),  # 上位机编码, string
        "user_code": params.get("user_code"),  # 员工编码, string
        "equip_code": params.get("equip_code"),  # 设备编码, string
        "process_code": params.get("process_code"),  # 工序编码, string, 必输
        "wip_list": params.get("wip_list"),
        "time": get_time(),
    }
    await insert_data(input_data)

    result = {
        "code": MesCode.success,  # 接口应答状态代码, success：成功; fail：失败, string
        "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
        "info": f"OK;{bom_check_result_msg}",  # 接口返回信息描述, 如果为fail, 则为fail的异常描述信息, string
    }

    return result


async def get_last_retrospect(last_procedure_code_list: list, qr_code_list: list) \
        -> Tuple[Dict[str, Dict], List]:
    """
    获取入参中在制品单间码在追溯表中前一道工序上的产出信息
    :param last_procedure_code_list:
    :param qr_code_list:
    :return:
    """
    qr_code_map = {rec: rec for rec in qr_code_list}
    pipeline = [
        {
            "$match": {
                "qr_code": {"$in": qr_code_list},
                "procedure_code": {"$in": last_procedure_code_list},
                "op_type": RetrospectType.PRODUCT_OUT,
                "is_delete": False,
            }
        },
        {"$sort": {"op_time": -1}},
        {"$group": {"_id": "$qr_code", "latestDocument": {"$first": "$$ROOT"}}},
    ]
    collection = get_retrospect_collection()
    result = {}
    async for rec in collection.aggregate(pipeline):
        qr_code = rec["_id"]
        document = rec["latestDocument"]
        result.update({qr_code: document})
        # 如果前道工序有该条码的产出(out)追溯信息，则从字典中pop该条码，最终字典中剩余的就是前道工序没有产出信息的条码
        qr_code_map.pop(qr_code)

    return result, list(qr_code_map.keys())


async def check_material(retrospect_records: Dict[str, Dict], bom_info: dict, process_code: str) -> Tuple[bool, str]:
    """
    校验在制品单件码在前道工序生产的物料信息是否与当前工序bom信息中投入的在制品信息是否一致；
    查询前一道工序追溯数据的check_result是否合格;
    BOM齐套性校验;
    :param process_code:
    :param retrospect_records:
    :param bom_info:
    :return:
    """
    # 获取当前工序bom信息中分类是在制品（半成品）的投入物料信息
    target_material_code_list = []
    # 获取当前工序bom信息中分类是在制品（半成品）的投入物料信息
    target_material_code_list_copy = {}
    # 获取当前工序BOM信息中投入物料分类是批次码的物料以及它的扣料数量
    target_material_code_quota_map = {}
    if bom_info and bom_info.get("bom_info"):
        for rec in bom_info["bom_info"].get("product_in", []):
            if rec.get("material_type") == "semi_finished_product":
                target_material_code_list.append(rec.get("material_code"))
                target_material_code_list_copy.update(
                    {rec.get("material_code"): rec.get("material_code")}
                )
            if rec.get("code_type") == "batch_code":
                target_material_code_quota_map.update(
                    {rec.get("material_code"): rec.get("material_quota")}
                )

    # 原料码值与投料数量的映射关系
    material_code_list: list = list(target_material_code_quota_map.keys())
    material_code_num_map: dict = await get_feeding_material_code_num_map(
        code_list=material_code_list, process_code=process_code
    )
    no_bom_material_info = []
    # 取消对码值前工序产出是否是NG品的校验
    # no_check_result_info = []
    no_target_num_info = []
    for qr_code, document in retrospect_records.items():
        material_code = document.get("material_code")
        # check_result = document.get("check_result")
        # 校验在制品单间码前道工序产出是否在本工序bom信息投入物料的在制品（半成品）中
        if material_code not in target_material_code_list:
            no_bom_material_info.append(qr_code)
        else:
            # 如果条码前工序产出是本工序bom投入料的在制品，则从‘target_material_code_list_copy’将该物料信息pop，
            # ‘target_material_code_list_copy’剩余的物料信息即为本工序bom需要的投入在制品物料但是入参条码的前工序产出物料中没有，
            # 即本工序投入物料不完整，无法开工
            if material_code in target_material_code_list_copy:
                # 可能多个在制品单件码对应同一个在制品，那么在校验第一个的时候，已经从  里把这个在制品编码pop了，
                # 第二个及之后的码值时，需要判断下是否还在，如果已经pop了，就不需要重复操作了；
                # 上一层if已经判断了码值是不是bom所需在制品，能走到这一层判断的都是bom需要的在制品。
                target_material_code_list_copy.pop(material_code)
            else:
                pass

        # 校验在制品单间码前道工序产出是否合格
        # if check_result == "不合格":
        #     no_check_result_info.append(qr_code)

    # 核对BOM信息中批次码的扣料数量与各物料当前系统中的投料数量的关系，如果扣料>投料，需要提示投料数量不足；
    for k, v in target_material_code_quota_map.items():
        if v > material_code_num_map.get(k, 0):
            no_target_num_info.append(k)

    # 用result来判断是不是必须提示的错误信息；投料数量由于允许扣减为负数，
    # 所以当这一项不满足校验时，仍然可以进站，不是强制报错，返回信息中提示即可
    result, error_msg = True, ""
    if no_bom_material_info:
        result = False
        error_msg += f"下列单件码进站物料不符合BOM配置:{no_bom_material_info};"
    # if no_check_result_info:
    #     result = False
    #     error_msg += f"下列单件码进站物料为NG品:{no_check_result_info};"
    if target_material_code_list_copy:
        result = False
        error_msg += f"BOM齐套性校验未通过, 本工序投入原料不齐套;"
    if no_target_num_info:
        result = False
        error_msg += f"下列物料的投料数量少于工序BOM扣减数量:{no_target_num_info};"

    return result, error_msg


async def insert_data(data):
    """
    登记当前投料蓝胶码/电芯码
    :param data:
    :return:
    """
    collection = get_mongo_collection(
        database_name=f"{get_organization()}_mes",
        collection_name=f"input_wip_instation_record",
    )
    await collection.insert_one(data)
